package com.leyou.search.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.common.dto.PageResult;
import com.leyou.common.exception.pojo.ExceptionEnum;
import com.leyou.common.exception.pojo.LyException;
import com.leyou.common.utils.BeanHelper;
import com.leyou.common.utils.JsonUtils;
import com.leyou.item.client.ItemClient;
import com.leyou.item.dto.SpuDTO;
import com.leyou.item.entity.*;
import com.leyou.search.dto.GoodsDTO;
import com.leyou.search.dto.SearchRequest;
import com.leyou.search.entity.Goods;
import com.leyou.search.repository.SearchRepository;
import com.leyou.search.utils.HighlightUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;

@Service
public class SearchService {

    @Autowired
    private SearchRepository searchRepository;

    @Autowired
    private ElasticsearchTemplate esTemplate;

    @Autowired
    private ItemClient itemClient;

    /**
     * 提供一个将SpuDTO对象转成Goods对象的业务方法
     */
    public Goods buildGoods(SpuDTO spuDTO){
        //根据SpuId查询Sku集合
        List<Sku> skus = itemClient.findSkusBySpuId(spuDTO.getId());
        //定义一个包含索引库中所需要的sku字段的List集合
        List<Map<String, Object>> skuList = new ArrayList<>();
        //遍历skus
        skus.forEach(sku -> {
            Map<String, Object> skuMap = new HashMap<>();
            skuMap.put("id", sku.getId());
            skuMap.put("image", StringUtils.substringBefore(sku.getImages(), ","));
            skuMap.put("price", sku.getPrice());
            skuMap.put("title", sku.getTitle().substring(spuDTO.getName().length()));
            //将skuMap放入skuList中
            skuList.add(skuMap);
        });

        //封装搜素条件字段all
        String all = spuDTO.getBrandName()//用户搜索的内如可以匹配品牌名称
                + spuDTO.getCategoryName()//用户搜索的内如可以匹配分类名称
                + skus.stream().map(Sku::getTitle).collect(Collectors.joining());//用户搜索的内如可以匹配Sku名称

        //封装价格的Set集合
        Set<Long> priceSet = skus.stream().map(Sku::getPrice).collect(Collectors.toSet());

        //初始化一个存放规格参数的Map
        Map<String, Object> specMap = new HashMap<>();
        //得到specMap的key值的来源，即当前商品所属分类下可以用来检索过滤的规格参数对象
        List<SpecParam> specParams = itemClient.findSpecParamByGid(null, spuDTO.getCid3(), true);
        //得到specMap的value值的来源，即查询当前Spu所对应的SpuDetail对象
        SpuDetail spuDetail = itemClient.findSpuDetailBySpuId(spuDTO.getId());
        //从SpuDetail对象中获取通用规格参数的来源字符串版
        String genericSpec = spuDetail.getGenericSpec();
        //将字符串版的通过规格参数转成json
        Map<Long, Object> genericSpecJson = JsonUtils.toMap(genericSpec, Long.class, Object.class);
        //从SpuDetail对象中获特有用规格参数的来源字符串版
        String specialSpec = spuDetail.getSpecialSpec();
        //将字符串版的特有规格参数转成json
        Map<Long, List<Object>> specialSpecJson = JsonUtils.nativeRead(specialSpec, new TypeReference<Map<Long, List<Object>>>() {});
        //说明，上面key值的来源List<SpecParam>集合有几个元素，最终specMap就有几个键值对
        specParams.forEach(specParam -> {
            String key = specParam.getName();
            Object value = null;
            //判断当前规格参数的值的来源，即判断当前规格参数是否为通用规格参数
            if(specParam.getGeneric()){
                //表示当前规格参数为通用规格参数
                value = genericSpecJson.get(specParam.getId());
            }else {
                //表示当前规格参数为特有规格参数
                value = specialSpecJson.get(specParam.getId());
            }
            //对所有为数字类型的规格参数进行处理
            if(specParam.getNumeric()){
                value = chooseSegment(value, specParam);
            }
            specMap.put(key, value);
        });

        //创建Goods对象
        Goods goods = new Goods();
        goods.setId(spuDTO.getId());
        goods.setSpuName(spuDTO.getName());
        goods.setSubTitle(spuDTO.getSubTitle());
        goods.setBrandId(spuDTO.getBrandId());
        goods.setCategoryId(spuDTO.getCid3());
        goods.setCreateTime(spuDTO.getCreateTime().getTime());
        goods.setSkus(JsonUtils.toString(skuList));
        goods.setAll(all);
        goods.setPrice(priceSet);
        goods.setSpecs(specMap);
        return goods;
    }


    /*此算法是将数字转成所在的区间*/
    private String chooseSegment(Object value, SpecParam p) {
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }
        double val = parseDouble(value.toString());
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    private double parseDouble(String str) {
        try {
            return Double.parseDouble(str);
        } catch (Exception e) {
            return 0;
        }
    }

    /**
     * 分页查询商品列表
     */
    public PageResult<GoodsDTO> goodsPageQuery(SearchRequest request) {
        //创建一个复杂条件查询所需要的查询构建器【有点类似于通用mapper中的Example对象】
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        //通过查询构建器QueryBuilder指定查询方式，.operator(Operator.AND)表示多个词条之间是and的关系
        nativeSearchQueryBuilder.withQuery(buildQueryParams(request));
        //设置分页条件
        nativeSearchQueryBuilder.withPageable(PageRequest.of(request.getPage()-1, request.getSize()));
        //指定查询的字段
        nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "spuName", "subTitle", "skus"}, null));
        //设置高亮字段域
        HighlightUtils.highlightField(nativeSearchQueryBuilder, "spuName");
        //这里使用复杂es查询中的聚合查询【分页查也属于聚合查询】
        AggregatedPage<Goods> aggGoods = esTemplate.queryForPage(nativeSearchQueryBuilder.build(), Goods.class, HighlightUtils.highlightBody(Goods.class, "spuName"));
        //获取商品列表信息
        List<Goods> content = aggGoods.getContent();
        if(CollectionUtils.isEmpty(content)){
            throw new LyException(ExceptionEnum.GOODS_NOT_FOUND);
        }
        //封装自定义的分页对象
        PageResult<GoodsDTO> pageResult = new PageResult<>(
                aggGoods.getTotalElements(),
                aggGoods.getTotalPages(),
                BeanHelper.copyWithCollection(content, GoodsDTO.class)
        );
        return pageResult;
    }

    /**
     * 构建查询条件，这里包含搜索条件和过滤条件。
     */
    private QueryBuilder buildQueryParams(SearchRequest request) {
        //得到一个可以实现过滤条件查询的对象
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        //封装条件查询
        queryBuilder.must(QueryBuilders.multiMatchQuery(request.getKey(), "all", "spuName").operator(Operator.AND));
        //得到所有过滤条件
        Map<String, Object> filterParams = request.getFilterParams();
        //判断有过滤条件才封装过滤查询
        if(!CollectionUtils.isEmpty(filterParams)){
            //遍历过滤条件
            filterParams.entrySet().forEach(entry->{
                String key = entry.getKey();
                Object value = entry.getValue();
                //改造key值
                if(StringUtils.equals(key, "分类")){
                    key = "categoryId";
                }else if(StringUtils.equals(key, "品牌")){
                    key = "brandId";
                }else {
                    key = "specs."+key+".keyword";
                }
                //封装过滤查询
                queryBuilder.filter(QueryBuilders.termQuery(key, value));
            });
        }
        return queryBuilder;
    }

    /**
     * 编写查询聚合条件业务代码
     */
    public Map<String, List<?>> queryFilterParamData(SearchRequest request) {
        //创建一个有序map，保证分类和品牌两个固定过滤条件在最上面
        Map<String, List<?>> filterParamMap = new LinkedHashMap<>();
        //提供一个封装复杂条件的对象
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        //添加查询条件
        nativeSearchQueryBuilder.withQuery(buildQueryParams(request));
        //可以做个优化，数据列表部分的个数指定为0，由于api限制，我们查询一个即可
        nativeSearchQueryBuilder.withPageable(PageRequest.of(0, 1));
        //给分类的聚合结果起个名字
        String categoryAgg = "categoryAgg";
        //封装分类的聚合条件，注意：默认显示十个聚合结果，如果要显示全部，就在最后加上.size(Integer.MAX_VALUE)
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(categoryAgg).field("categoryId"));
        //给品牌的聚合结果起个名字
        String brandAgg = "brandAgg";
        //封装品牌的聚合条件
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(brandAgg).field("brandId"));
        //执行聚合查询
        AggregatedPage<Goods> goodsAgg = esTemplate.queryForPage(nativeSearchQueryBuilder.build(), Goods.class);
        //获取到所有的聚合结果
        Aggregations aggregations = goodsAgg.getAggregations();
        //得到分类的聚合结果
        Terms categoryTerms = aggregations.get(categoryAgg);
        //先把所有聚合结果的分类id集合转成分类对象集合，再赋值给filterParamMap
        List<Long> cids = handlerCategoryTerms(filterParamMap, categoryTerms);
        //得到品牌的聚合结果
        Terms brandTerms = aggregations.get(brandAgg);
        //先把所有聚合结果的品牌id集合转成品牌对象集合，再赋值给filterParamMap
        handlerBrandTerms(filterParamMap, brandTerms);
        //通过分类聚合结果得到所需要聚合的规格参数并将动态规格参数过滤条件结果加入到map中
        handlerSpecParamFilterData(filterParamMap, cids, buildQueryParams(request));
        return filterParamMap;
    }

    /**
     * 封装动态规格参数过滤条件业务方法
     * @param filterParamMap  封装后的结果map
     * @param cids  分类id集合，来确定要封装哪些规格参
     * @param buildQueryParams  聚合查询所需要的查询条件
     */
    private void handlerSpecParamFilterData(Map<String, List<?>> filterParamMap, List<Long> cids, QueryBuilder buildQueryParams) {
        cids.forEach(cid->{
            //提供一个封装复杂条件的对象
            NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
            //添加查询条件
            nativeSearchQueryBuilder.withQuery(buildQueryParams);
            //可以做个优化，数据列表部分的个数指定为0，由于api限制，我们查询一个即可
            nativeSearchQueryBuilder.withPageable(PageRequest.of(0, 1));

            //根据分类查询规格参数
            List<SpecParam> specParams = itemClient.findSpecParamByGid(null, cid, true);
            //遍历specParams，并对每一个规格参数在索引库中的name进行聚合条件封装
            specParams.forEach(specParam -> {
                //给每一个规格参数的聚合结果起个名字
                String specParamAgg = specParam.getName();
                String field = "specs."+specParam.getName()+".keyword";
                //封装当前规格参数的聚合条件，注意：默认显示十个聚合结果，如果要显示全部，就在最后加上.size(Integer.MAX_VALUE)
                nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(specParamAgg).field(field));
            });
            //执行聚合查询
            AggregatedPage<Goods> goodsAgg = esTemplate.queryForPage(nativeSearchQueryBuilder.build(), Goods.class);
            //获取到所有的聚合结果
            Aggregations aggregations = goodsAgg.getAggregations();
            //遍历specParams取出聚合结果
            specParams.forEach(specParam -> {
                //给每一个规格参数的聚合结果起个名字
                String specParamAgg = specParam.getName();
                //得到规格参数过滤条件的聚合结果
                Terms specParamTerms = aggregations.get(specParamAgg);
                //获取所有聚合结构
                List<String> specFilterList = specParamTerms.getBuckets()
                        .stream()
                        .map(Terms.Bucket::getKeyAsString)
                        .collect(Collectors.toList());
                //将当前规格参数过滤条件放入结果map中
                filterParamMap.put(specParamAgg, specFilterList);
            });
        });
    }

    /**
     * 封装品牌过滤条件
     */
    private void handlerBrandTerms(Map<String, List<?>> filterParamMap, Terms brandTerms) {
        //获取品牌id的集合
        List<Long> brandIds = brandTerms.getBuckets()
                .stream()
                .map(Terms.Bucket::getKeyAsNumber)
                .map(Number::longValue)
                .collect(Collectors.toList());
        //根据品牌id的集合批量查询品牌对象的集合
        List<Brand> brands = itemClient.findBrandsByIds(brandIds);
        //将品牌的聚合结果放入结果map
        filterParamMap.put("品牌", brands);
    }

    /**
     * 封装分类过滤条件
     * ?号表示通配符，什么类型都可以
     * Object是对象类型
     * T泛型
     * 这里，返回分类id的集合，是为了使用每个分类id去获取当前分类下所有可以用来搜索的规格参数
     */
    private List<Long> handlerCategoryTerms(Map<String, List<?>> filterParamMap, Terms categoryTerms) {
        //获取所有聚合结果的id的集合
        List<Long> categoryIds = categoryTerms.getBuckets()
                .stream()
                .map(Terms.Bucket::getKeyAsNumber)
                .map(Number::longValue)
                .collect(Collectors.toList());
        //根据分类id的集合批量查询分对象
        List<Category> categories = itemClient.findCategoriesByIds(categoryIds);
        //把获取到的分类对象的集合放入结果map中
        filterParamMap.put("分类", categories);
        return categoryIds;
    }

    /**
     * 注意mq的业务方法，可以不处理器异常，不管什么异常，跑出去，mq就会回执，重新执行。
     */
    public void addIndex(Long id) {
        SpuDTO spuDTO = itemClient.findSpuDTOBySpuId(id);
        Goods goods = buildGoods(spuDTO);
        searchRepository.save(goods);
    }

    public void delIndex(Long id) {
        searchRepository.deleteById(id);
    }
}
